/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */

package cn.hermit.bean;

import cn.hermit.reflection.parameter.ParameterNameFinder;
import cn.hermit.spi.SPILoader;
import cn.hermit.util.ReflectionUtils;
import cn.hermit.util.SpringUtils;
import cn.hermit.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author JackGao 高建国
 * @description //TODO 设计说明
 * @date 3/6/2017
 */
public class BeanFactory {

    private static Set<Object> factories;

    private static final Logger logger = Logger.getLogger(BeanFactory.class.getName());

    private static ConcurrentMap<String, Object> beanMap = new ConcurrentHashMap<>();

    private static ConcurrentMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();

    private static List<BeanListener> beanListeners;

    static {
        try {
            beanListeners = SPILoader.load(BeanListener.class);
        } catch (Exception e) {
            //ignored
        }
    }

    public static void register(String name, Object obj) {
        beanMap.putIfAbsent(name, obj);
    }

    public static void register(String name, BeanDefinition definition) {
        beanDefinitionMap.putIfAbsent(name, definition);
    }

    public static void registerConfigClz(Class<?> configCLz) {
        Method[] methods = configCLz.getDeclaredMethods();
        for (Method method : methods) {
            int modifiers = method.getModifiers();
            if (Modifier.isPrivate(modifiers)) {
                continue;
            }
            BeanDefinition definition = new BeanDefinition();
            definition.setConfigMethod(method);
        }
    }

    public static Object getBean(String name) {
        Object bean = beanMap.get(name);
        if (bean == null) {
            bean = getBeanFromFactories(name);
        }
        if (bean == null) {
            bean = createBean(name);
        }
        return bean;
    }

    public static Object getBean(Class<?> clz) {
        String name = clz.getName();
        Object bean = beanMap.get(name);
        if (bean == null) {
            bean = getBeanFromFactories(clz);
        }
        if (bean == null) {
            bean = getBeanFromFactories(name);
        }
        if (bean == null) {
            name = clz.getSimpleName();
            bean = beanMap.get(name);
        }
        if (bean == null) {
            for (Object obj : beanMap.values()) {
                if (clz.isInstance(obj.getClass())) {
                    bean = obj;
                }
            }
        }
        if (bean == null) {
            for (Object obj : beanMap.values()) {
                if (clz.isAssignableFrom(obj.getClass())) {
                    bean = obj;
                }
            }
        }
        if (bean == null) {
            try {
                bean = clz.newInstance();
                populateBean(bean, null);
                beanMap.putIfAbsent(clz.getName(), bean);
            } catch (ReflectiveOperationException e) {
                logger.log(Level.SEVERE, e.getMessage(), e);
            }
        }
        return bean;
    }

    public static <T> List<T> getBeanList(Class<T> clz) {
        List<T> beanList = new ArrayList<>();
        for (Object obj : beanMap.values()) {
            if (clz.isAssignableFrom(obj.getClass())) {
                beanList.add((T) obj);
            }
        }
        if (beanList.size() == 0) {
            Object bean = getBeanFromFactories(clz);
            if (bean != null) {
                beanList.add((T) bean);
            }
        }
        return beanList;
    }

    private static Object createBean(String name) {
        BeanDefinition definition = beanDefinitionMap.get(name);
        if (definition == null) {
            return null;
        }
        //TODO It is a very simple way
        Object bean = null;
        try {
            ClassLoader clzLoader = Thread.currentThread().getContextClassLoader();
            if (definition.getConfigMethod() != null) {
                Method method = definition.getConfigMethod();
                Class<?> configClz = method.getDeclaringClass();
                int modifiers = method.getModifiers();
                Object target = null;
                if (Modifier.isStatic(modifiers)) {
                    target = configClz;
                } else {
                    Object configBean = getBean(configClz.getName());
                    if (configBean == null) {
                        configBean = configClz.newInstance();
                        populateBean(configBean, null);
                        beanMap.putIfAbsent(configClz.getName(), configBean);
                    }
                    target = configBean;
                }
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes == null || parameterTypes.length == 0) {
                    bean = method.invoke(target);
                } else {
                    Object[] paramValues = new Object[parameterTypes.length];
                    String[] paramNames = ReflectionUtils.getParameterNames(method);
                    for (int i = 0; i < parameterTypes.length; i++) {
                        Class<?> paramType = parameterTypes[i];
                        String paramName = paramNames[i];
                        Object paramValue = null;
                        if (paramName.startsWith(ParameterNameFinder.PARAM_DEFAULT_PREFIX)) {
                            paramValue = getBean(paramType);
                        } else {
                            paramValue = getBean(paramName);
                        }
                        paramValues[i] = paramValue;
                    }
                    bean = method.invoke(target, paramValues);
                }
            } else {
                Class<?> clz = definition.getClz();
                if (clz == null && !StringUtils.isBlank(definition.getClzName())) {
                    clz = clzLoader.loadClass(definition.getClzName());
                }
                bean = clz.newInstance();
            }
            if (bean != null) {
                populateBean(bean, definition);
                beanMap.putIfAbsent(name, bean);
            }
        } catch (Exception e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
        }
        return bean;
    }

    public static void populateBean(Object bean) {
        populateBean(bean, null);
    }

    private static void populateBean(Object bean, BeanDefinition definition) {
        Field[] fields = bean.getClass().getDeclaredFields();
        if (fields.length == 0) {
            return;
        }
        Map<String, Object> properties = null;
        if (definition != null) {
            properties = definition.getProperties();
        }
        for (Field field : fields) {
            int modifiers = field.getModifiers();
            if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) {
                continue;
            }
            BeanEvent event = new BeanEvent();
            event.setType(BeanEvent.Type.BEFORE_FIELD_ASSIGNED);
            event.put(BeanEvent.KEY_FIELD, field);
            publishEvent(event);

            String name = field.getName();
            Object fieldValue = null;
            if (properties != null) {
                fieldValue = properties.get(name);
            }
            Class<?> fieldType = field.getType();
            if (!fieldType.getName().startsWith("java")) {
                if (fieldValue == null) {
                    fieldValue = getBean(name);
                }
                if (fieldValue == null) {
                    fieldValue = getBean(fieldType);
                }
            }
            if (fieldValue == null) {
                continue;
            }
            ReflectionUtils.setFieldValue(field, fieldValue, properties);
        }
    }

    public static void addFactory(Object... factory) {
        if (factory == null || factory.length == 0) {
            return;
        }
        if (factories == null) {
            factories = new ConcurrentSkipListSet<>();
        }
        factories.addAll(Arrays.asList(factory));
    }

    private static Object getBeanFromFactories(String name) {
        if (factories == null || factories.size() == 0) {
            return null;
        }
        Object bean = null;
        for (Object factory : factories) {
            if (SpringUtils.SPRING_BEAN_FACTORY_CLASS.equals(factory.getClass())) {
                bean = SpringUtils.getBean(factory, name);
            }

            if (bean != null) {
                break;
            }
        }
        return bean;
    }

    private static Object getBeanFromFactories(Class<?> clz) {
        if (factories == null || factories.size() == 0) {
            return null;
        }
        Object bean = null;
        for (Object factory : factories) {
            if (SpringUtils.SPRING_BEAN_FACTORY_CLASS.equals(factory.getClass())) {
                bean = SpringUtils.getBean(factory, clz);
            }

            if (bean != null) {
                break;
            }
        }
        return bean;
    }

    private static void publishEvent(BeanEvent event) {
        if(beanListeners != null || beanListeners.size() > 0) {
            for(BeanListener beanListener : beanListeners) {
                beanListener.handleEvent(event);
            }
        }
    }
}
